package main

import (
	"3nets.io/wireguardcommon"
	"crypto"
	"encoding/base64"
	"fmt"
	"github.com/vishvananda/netlink"
	"net"
	"os"
)

const (
	ConnHost = "0.0.0.0"
	ConnPort = "32323"
	ConnType = "tcp"
)

var managerPrivate crypto.PrivateKey
var managerPublic crypto.PublicKey
var wireguardLink netlink.Link

func main() {
	var err error
	managerPrivate, managerPublic, err = wireguardcommon.GenerateKeyPair()
	if err != nil {
		fmt.Println("Error initializing keypair:", err.Error())
		os.Exit(1)
	}

	managerIp, _ := wireguardcommon.GetFirstAddr()
	fmt.Printf("managerIP : %s\n", managerIp)
	_, wireguardLink, err = wireguardcommon.CreateWireguardInterface(managerPrivate, managerIp.String())
	if err != nil {
		return
	}

	///backgrouding the process
	///////////////////////////////////

	/*
		logLevel := func() int {
			switch os.Getenv("LOG_LEVEL") {
			case "verbose", "debug":
				return device.LogLevelVerbose
			case "error":
				return device.LogLevelError
			case "silent":
				return device.LogLevelSilent
			}
			return device.LogLevelError
		}()
		logger := device.NewLogger(
			logLevel,
			fmt.Sprintf("(%s) ", wireguardcommon.InterfaceName),
		)

		logger.Verbosef("Starting ..")

		env := os.Environ()
		files := [3]*os.File{}
		if os.Getenv("LOG_LEVEL") != "" && logLevel != device.LogLevelSilent {
			files[0], _ = os.Open(os.DevNull)
			files[1] = os.Stdout
			files[2] = os.Stderr
		} else {
			files[0], _ = os.Open(os.DevNull)
			files[1], _ = os.Open(os.DevNull)
			files[2], _ = os.Open(os.DevNull)
		}

		attr := &os.ProcAttr{
			Files: []*os.File{
				files[0], // stdin
				files[1], // stdout
				files[2], // stderr
			},
			Dir: ".",
			Env: env,
		}
		path, err := os.Executable()
		if err != nil {
			logger.Errorf("Failed to determine executable: %v", err)
			os.Exit(wireguardcommon.ExitSetupFailed)
		}

		process, err := os.StartProcess(
			path,
			os.Args,
			attr,
		)
		if err != nil {
			logger.Errorf("Failed to daemonize: %v", err)
			os.Exit(wireguardcommon.ExitSetupFailed)
		}
		process.Release()
		///End of backgrouding the process
		///////////////////////////////////

	*/
	// Listen for incoming connections.
	l, err := net.Listen(ConnType, ConnHost+":"+ConnPort)
	if err != nil {
		fmt.Println("Error listening:", err.Error())
		os.Exit(1)
	}
	// Close the listener when the application closes.
	defer func(l net.Listener) {
		err := l.Close()
		if err != nil {
			fmt.Println("error closing listener on " + ConnHost + ":" + ConnPort)
		}
	}(l)
	fmt.Println("Listening on " + ConnHost + ":" + ConnPort)
	for {
		// Listen for an incoming connection.
		conn, err := l.Accept()
		if err != nil {
			fmt.Println("Error accepting: ", err.Error())
			os.Exit(1)
		}
		// Handle connections in a new goroutine.
		go handleRequest(conn)
	}

	return
}

// Handles incoming requests.
func handleRequest(conn net.Conn) {
	maxReqLen := 128
	edgeSignature, responseLen, err := wireguardcommon.ReadConn(conn, maxReqLen)

	if err != nil {
		return
	}

	ok, err := wireguardcommon.VerifySignature("", "3netsedge.io", edgeSignature[:responseLen])
	if !ok || err != nil {
		wireguardcommon.CloseConn(conn)
		return
	}

	var managerPrivateBytes [32]byte
	var managerPublicBytes [32]byte
	if ok := wireguardcommon.CheckType(&managerPrivateBytes, managerPrivate); !ok {
		panic("ecdh: unexpected type of manager public key")
	}
	if ok := wireguardcommon.CheckType(&managerPublicBytes, managerPublic); !ok {
		panic("ecdh: unexpected type of manager public key")
	}

	managerDHPrivate := base64.StdEncoding.EncodeToString(managerPrivateBytes[:])
	managerDHPublic := base64.StdEncoding.EncodeToString(managerPublicBytes[:])
	fmt.Printf("managerDHPublic: %s\n", managerDHPublic)
	fmt.Printf("managerDHPrivate: %s\n", managerDHPrivate)

	// Send manager DH public
	writeLen, err := wireguardcommon.WriteConn(conn, []byte(managerDHPublic))
	if err != nil || writeLen != len(managerDHPublic) {
		fmt.Printf("write length mismatch. expected = %d, got=%d\n", len(managerDHPublic), writeLen)
		return
	}

	edgeDHPublic, responseLen, err := wireguardcommon.ReadConn(conn, maxReqLen)
	fmt.Printf("edgeDHPublic: %s\n", edgeDHPublic)
	presharedKey, err := wireguardcommon.GenPskOnManager(edgeDHPublic, managerPrivate)
	if err != nil {
		fmt.Printf("Failed to generate manager private/public key pair: %s\n", err)
		return
	} else {
		fmt.Printf("presharedkey=%s\n", presharedKey)
	}

	///////////////////////////////
	// derive edge keys         //
	/////////////////////////////
	_, edgePublicStr, err := wireguardcommon.CreateKeyPairFromSeed(edgeSignature)
	if err != nil {
		fmt.Printf("Failed to generate manager private/public key pair: %s\n", err)
		return
	}
	///////////////////////////////
	// end of derive edge keys  //
	/////////////////////////////

	unusedIp, err := wireguardcommon.FindUnusedIp() //"192.168.88.3/32"
	allowedIp := unusedIp.String() + "/32"
	if err == nil {
		fmt.Printf("found an unused ip=%s\n", allowedIp)
		secret, _ := base64.StdEncoding.DecodeString(presharedKey)
		ack, _ := wireguardcommon.Chacha20poly1305Encrypt(secret, []byte("OK:"+wireguardcommon.Ip2+":"+allowedIp))

		fmt.Printf("sending ack %x", ack)
		writeLen, err = wireguardcommon.WriteConn(conn, ack)
		if err != nil || writeLen != len(ack) {
			fmt.Printf("write length mismatch. expected = %d, got=%d\n", len(ack), writeLen)
			return
		}

		wireguardcommon.AddPeer(presharedKey, edgePublicStr, "", allowedIp, false)
		managerIp, _ := wireguardcommon.GetFirstAddr()
		err := wireguardcommon.AddLinkAddress(wireguardLink, managerIp.String(), allowedIp)
		if err != nil {
			wireguardcommon.CloseConn(conn)
			return
		}
	} else {
		fmt.Printf("could not find an unused ip\n")
	}

	// Close the connection when you're done with it.
	wireguardcommon.CloseConn(conn)
}
